package com.orhanobut.logger;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.orhanobut.logger.Utils.checkNotNull;

/**
 * Draws borders around the given log message along with additional information such as :
 *
 * <ul>
 *   <li>Thread information</li>
 *   <li>Method stack trace</li>
 * </ul>
 *
 * <pre>
 *  ┌──────────────────────────
 *  │ Method stack history
 *  ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 *  │ Thread information
 *  ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 *  │ Log message
 *  └──────────────────────────
 * </pre>
 *
 * <h3>Customize</h3>
 * <pre><code>
 *   FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
 *       .showThreadInfo(false)  // (Optional) Whether to show thread info or not. Default true
 *       .methodCount(0) // (Optional) How many method line to show. Default 2
 *       .methodOffset(7) // (Optional) Hides internal method calls up to offset. Default 5
 *       .logStrategy(customLog) // (Optional) Changes the log strategy to print out. Default LogCat
 *       .tag("My custom tag")   // (Optional) Global tag for every log. Default PRETTY_LOGGER
 *       .build();
 * </code></pre>
 */
public class PrettyFormatStrategy implements FormatStrategy {
    /**
     * max limit for a log entry is ~4076 bytes,
     * so 4000 bytes is used as chunk size since default charset
     * is UTF-8
     */
    private static final int CHUNK_SIZE = 4000;

    /**
     * The minimum stack trace index, starts at this class after two native calls.
     */
    private static final int MIN_STACK_OFFSET = 5;

    /**
     * Drawing toolbox
     */
    private static final char TOP_LEFT_CORNER = '┌';
    private static final char BOTTOM_LEFT_CORNER = '└';
    private static final char MIDDLE_CORNER = '├';
    private static final char HORIZONTAL_LINE = '│';
    private static final String DOUBLE_DIVIDER = "────────────────────────────────────────────────────────";
    private static final String SINGLE_DIVIDER = "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄";
    private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
    private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
    private static final String MIDDLE_BORDER = MIDDLE_CORNER + SINGLE_DIVIDER + SINGLE_DIVIDER;

    private final int methodCount;
    private final int methodOffset;
    private final boolean showThreadInfo;
    @NotNull private final LogStrategy logStrategy;
    @Nullable private final String tag;
    private final boolean autoTag;

    private PrettyFormatStrategy(@NotNull Builder builder) {
        checkNotNull(builder);

        methodCount = builder.methodCount;
        methodOffset = builder.methodOffset;
        showThreadInfo = builder.showThreadInfo;
        logStrategy = builder.logStrategy;
        tag = builder.tag;
        autoTag = builder.autoTag;
    }

    /**
     * Builder method
     *
     * @return Builder
     */
    @NotNull public static Builder newBuilder() {
        return new Builder();
    }

    @Override
    public void log(int priority, @Nullable String onceOnlyTag, @NotNull String message) {
        checkNotNull(message);
        String tag = formatTag(onceOnlyTag);
        logTopBorder(priority, tag);
        logHeaderContent(priority, tag, methodCount);
        byte[] bytes = message.getBytes();
        int length = bytes.length;
        if (length <= CHUNK_SIZE) {
            if (methodCount > 0) {
                logDivider(priority, tag);
            }
            logContent(priority, tag, message);
            logBottomBorder(priority, tag);
            return;
        }

        if (methodCount > 0) {
            logDivider(priority, tag);
        }
        for (int i = 0; i < length; i += CHUNK_SIZE) {
            int count = Math.min(length - i, CHUNK_SIZE);
            logContent(priority, tag, new String(bytes, i, count));
        }
        logBottomBorder(priority, tag);
    }

    @Override
    public void log(int priority, @Nullable String tag, String format, Object... args) {

    }

    private void logTopBorder(int logType, @Nullable String tag) {
    logChunk(logType, tag, TOP_BORDER);
  }

  @SuppressWarnings("StringBufferReplaceableByString")
  private void logHeaderContent(int logType, @Nullable String tag, int methodCount) {
    StackTraceElement[] trace = Thread.currentThread().getStackTrace();
    if (showThreadInfo) {
      logChunk(logType, tag, HORIZONTAL_LINE + " Thread: " + Thread.currentThread().getName());
      logDivider(logType, tag);
    }
    String level = "";

    int stackOffset = getStackOffset(trace) + methodOffset;

    //corresponding method count with the current stack may exceeds the stack trace. Trims the count
    if (methodCount + stackOffset > trace.length) {
      methodCount = trace.length - stackOffset - 1;
    }

    for (int i = methodCount; i > 0; i--) {
      int stackIndex = i + stackOffset;
      if (stackIndex >= trace.length) {
        continue;
      }
      StringBuilder builder = new StringBuilder();
      builder.append(HORIZONTAL_LINE)
          .append(' ')
          .append(level)
          .append(getSimpleClassName(trace[stackIndex].getClassName()))
          .append(".")
          .append(trace[stackIndex].getMethodName())
          .append(" ")
          .append(" (")
          .append(trace[stackIndex].getFileName())
          .append(":")
          .append(trace[stackIndex].getLineNumber())
          .append(")");
      level += "   ";
      logChunk(logType, tag, builder.toString());
    }
  }

  private void logBottomBorder(int logType, @Nullable String tag) {
    logChunk(logType, tag, BOTTOM_BORDER);
  }

  private void logDivider(int logType, @Nullable String tag) {
    logChunk(logType, tag, MIDDLE_BORDER);
  }

    private void logContent(int logType, @Nullable String tag, @NotNull String chunk) {
        checkNotNull(chunk);

    String[] lines = chunk.split(System.getProperty("line.separator"));
    for (String line : lines) {
      logChunk(logType, tag, HORIZONTAL_LINE + " " + line);
    }
  }

    private void logChunk(int priority, @Nullable String tag, @NotNull String chunk) {
        checkNotNull(chunk);

    logStrategy.log(priority, tag, chunk);
  }

    private String getSimpleClassName(@Nullable String name) {
        checkNotNull(name);

    int lastIndex = name.lastIndexOf(".");
    return name.substring(lastIndex + 1);
  }

    /**
     * Determines the starting index of the stack trace, after method calls made by this class.
     *
     * @param trace the stack trace
     * @return the stack offset
     */
    private int getStackOffset(@NotNull StackTraceElement[] trace) {
        checkNotNull(trace);

        for (int i = MIN_STACK_OFFSET; i < trace.length; i++) {
            StackTraceElement e = trace[i];
            String name = e.getClassName();
            if (!name.equals(LoggerPrinter.class.getName()) && !name.equals(Logger.class.getName())) {
                return --i;
            }
        }
        return -1;
    }

    /**
     * format the tag
     *
     * @param tag takes tag
     * @return tag og this instance
     */
    @Nullable private String formatTag(@Nullable String tag) {
        String curTag;
        if (this.autoTag) {
            StackTraceElement[] stackTrace = new Throwable().getStackTrace();
            if (stackTrace.length <= CALL_STACK_INDEX) {
                curTag = this.tag;
            } else {
                curTag = createStackElementTag(stackTrace[CALL_STACK_INDEX]);
                if ("Logger".equals(curTag)) {
                    curTag = createStackElementTag(stackTrace[CALL_STACK_INDEX+1]);
                }
            }
        } else {
            curTag = this.tag;
        }

        if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) {
            return curTag + "-" + tag;
        }
        return curTag;
    }


    private static final Pattern ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$");
    private static final int MAX_TAG_LENGTH = 64;

    private static final int CALL_STACK_INDEX = 7;

    /**
     * Extract the tag which should be used for the message from the {@code element}. By default
     * this will use the class name without any anonymous class suffixes (e.g., {@code Foo$1}
     * becomes {@code Foo}).
     */
    @Nullable protected String createStackElementTag(@NotNull StackTraceElement element) {
        String tag = element.getClassName();
        Matcher m = ANONYMOUS_CLASS.matcher(tag);
        if (m.find()) {
            tag = m.replaceAll("");
        }
        tag = tag.substring(tag.lastIndexOf('.') + 1);
        // Tag length limit was removed in API 24.
        //      if (tag.length() <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        //        return tag;
        //      }
        return tag.length() > MAX_TAG_LENGTH ? tag.substring(0, MAX_TAG_LENGTH) : tag;
    }
    /**
     * Builder class
     */
    public static class Builder {
        int methodCount = 2;
        int methodOffset = 0;
        boolean showThreadInfo = true;
        @Nullable LogStrategy logStrategy;
        @Nullable String tag = "PRETTY_LOGGER";
        boolean autoTag = true;

        /**
         * Builder constructor
         */
        private Builder() {
        }

        /**
         * assign Integer value to methodCount
         *
         * @param val takes count
         * @return this
         */
        @NotNull public Builder methodCount(int val) {
            methodCount = val;
            return this;
        }

        /**
         * assign Integer value to methodOffset
         *
         * @param val takes offset
         * @return this
         */
        @NotNull public Builder methodOffset(int val) {
            methodOffset = val;
            return this;
        }

        /**
         * assign boolean value to showThreadInfo
         *
         * @param val takes boolean to display thread info
         * @return this
         */
        @NotNull public Builder showThreadInfo(boolean val) {
            showThreadInfo = val;
            return this;
        }

        /**
         * assign LogStrategy value to logStrategy
         *
         * @param val takes LogStrategy
         * @return this
         */
        @NotNull public Builder logStrategy(@Nullable LogStrategy val) {
            logStrategy = val;
            return this;
        }

        /**
         * assign tag
         *
         * @param tag takes tag
         * @return this
         */
        @NotNull public Builder tag(@Nullable String tag) {
            this.tag = tag;
            this.autoTag = false;
            return this;
        }

        /**
         * 自动使用调用栈元素作为tag，参考Timber的实现
         *
         * @param autoTag
         * @return
         */
        @NotNull public Builder autoTag(boolean autoTag) {
            this.autoTag = autoTag;
            return this;
        }

        /**
         * pass context to PrettyFormatStrategy
         *
         * @return new PrettyFormatStrategy(this)
         */
        @NotNull public PrettyFormatStrategy build() {
            if (logStrategy == null) {
                logStrategy = new LogcatLogStrategy();
            }
            return new PrettyFormatStrategy(this);
        }
    }

}


